﻿using System;
using System.Collections.Generic;
using OverStore.Exceptions;
using OverStore.Runtime.OverStoreContext;
using OverStore.Runtime.PersistenceServiceProvider;
using OverStore.Scoping;
using OverStore.Runtime.Storage;

namespace OverStore.Runtime
{    
    /// <summary>
    /// Default implementation of <see cref="IPersistenceSession"/>.
    /// </summary>
    public sealed class OverStorePersistenceSession : IPersistenceSession
    {
        private IPersistenceServiceProvider _Provider;
        private readonly IDataStorage _Storage;
        private bool _Disposed;

        /// <summary>
        /// Initializes new instance of <see cref="OverStorePersistenceSession"/> class.
        /// </summary>
        /// <param name="context">Object contains partial configuration information.</param>
        public OverStorePersistenceSession(OverStoreContextImplementation context)
        {
            context.CurrentSession = this;
            if (context.DataStorage == null)
                throw new ArgumentException("Data Storage is missing on specified context instance.", "context");
            _Provider = context.PersistenceServiceProvider;
            _Storage = context.DataStorage;
        }

        #region IDisposable implementation.

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            _Disposed = true;
            var disposableProvider = _Provider as IDisposable;
            if (disposableProvider != null)
                disposableProvider.Dispose();
        }

        #endregion

        #region Instance

        /// <summary>
        /// Reloads object data, including aggregates, and reject all changes made to object. If object was marked as deleted,
        /// the mark is cleared.
        /// Method returns <c>true</c> if instance data was successfuly reloaded,
        /// <c>false</c> if there are no object data in data storage.
        /// </summary>
        /// <typeparam name="TInstance">The type of the instance to reload.</typeparam>
        /// <param name="instance">The instance to reload.</param>
        /// <returns></returns>
        /// <exception cref="System.ArgumentNullException"><paramref name="instance"/> is null reference.</exception>
        /// <exception cref="TypePersistenceNotSupportedException">The persistence of specific type not supported.</exception>
        /// <exception cref="PersistenceOperationFailedException">The error is occured during data source operation</exception>
        /// <exception cref="IdentityReferenceIntegrityException">Different instance with same identifier already registered in session.</exception>
        /// <exception cref="ObjectDisposedException">Object is disposed.</exception>
        public bool Reload<TInstance>(TInstance instance) where TInstance : class
        {
            CheckIsNotDisposed();
            if (instance == null)
                throw new ArgumentNullException("instance");
            using (var scope = OpenScope())
            {
                bool result = false;
                try
                {
                    result = _Provider.Reload<TInstance>(instance);
                }
                catch (PersistingRuntimeException ex)
                {
                    throw new PersistenceOperationFailedException(ex);
                }
                scope.Complete();
                return result;
            }
        }

        /// <summary>
        /// Clears specified collection and fills it up with object are read from parameterless reader.
        /// An item of collection is not reloaded.
        /// </summary>
        /// <typeparam name="TInstance">The type of the item of collection to reload.</typeparam>
        /// <param name="collection">The collection to reload.</param>
        /// <exception cref="System.ArgumentNullException"><paramref name="collection"/> is null reference.</exception>
        /// <exception cref="TypePersistenceNotSupportedException">The persistence of specific type not supported.</exception>
        /// <exception cref="PersistenceOperationFailedException">The error is occured during data source operation</exception>
        /// <exception cref="IdentityReferenceIntegrityException">Different instance with same identifier already registered in session.</exception>
        /// <exception cref="ObjectDisposedException">Object is disposed.</exception>
        public void ReloadCollection<TInstance>(IEnumerable<TInstance> collection) where TInstance : class
        {
            CheckIsNotDisposed();
            if (Object.ReferenceEquals(collection, null))
                throw new ArgumentNullException("collection");
            using (var scope = OpenScope())
            {
                try
                {
                    _Provider.ReloadCollection<TInstance>(collection);
                }
                catch (PersistingRuntimeException ex)
                {
                    throw new PersistenceOperationFailedException(ex);
                }
                scope.Complete();
            }
        }

        /// <summary>
        /// Clears specified collection and fills it up with object are read from parameterized reader with specified parameters.
        /// An item of collection is not reloaded.
        /// </summary>
        /// <typeparam name="TInstance">The type of the item of collection to reload.</typeparam>
        /// <typeparam name="TParameter"></typeparam>
        /// <param name="collection">The collection to reload.</param>
        /// <param name="parameter"></param>
        /// <exception cref="System.ArgumentNullException"><paramref name="collection"/> is null reference.</exception>
        /// <exception cref="TypePersistenceNotSupportedException">The persistence of specific type not supported.</exception>
        /// <exception cref="PersistenceOperationFailedException">The error is occured during data source operation</exception>
        /// <exception cref="IdentityReferenceIntegrityException">Different instance with same identifier already registered in session.</exception>
        /// <exception cref="ObjectDisposedException">Object is disposed.</exception>
        public void ReloadCollection<TInstance, TParameter>(IEnumerable<TInstance> collection, TParameter parameter) 
            where TInstance : class
            where TParameter : class
        {
            CheckIsNotDisposed();
            if (Object.ReferenceEquals(collection, null))
                throw new ArgumentNullException("collection");
            if (Object.ReferenceEquals(parameter, null))
                throw new ArgumentNullException("parameter");
            using (var scope = OpenScope())
            {
                try
                {
                    _Provider.ReloadCollection<TInstance, TParameter>(collection, parameter);
                }
                catch (PersistingRuntimeException ex)
                {
                    throw new PersistenceOperationFailedException(ex);
                }
                scope.Complete();
            }
        }

        /// <summary>
        /// Save object changes into storage, including aggregates, and accept object changes.
        /// </summary>
        /// <typeparam name="TInstance">OverrideDbType of object to save.</typeparam>
        /// <param name="instance">Instance to save.</param>
        /// <exception cref="System.ArgumentNullException">Instance is null.</exception>
        /// <exception cref="TypePersistenceNotSupportedException">The persistence of specific type is not supported.</exception>
        /// <exception cref="PersistenceOperationFailedException">The error is occured during data source operation</exception>
        /// <exception cref="VersionConflictException">The store of object failed because newest version of object already exist in data storage.</exception>
        /// <exception cref="IdentityReferenceIntegrityException">Different instance with same identifier already registered in session.</exception>
        /// <exception cref="ObjectDisposedException">Object is disposed.</exception>
        /// <exception cref="PersistenceOperationIsNotSupportedException">Operation is not supported because of configuration part responsible for this operation is missing or does not support specific functionallity.</exception>
        public void Save<TInstance>(TInstance instance)
            where TInstance : class
        {
            CheckIsNotDisposed();
            if (instance == null)
                throw new ArgumentNullException("instance");
            using (var scope = OpenScope())
            {
                try
                {
                    _Provider.Save<TInstance>(instance);
                }
                catch (PersistingRuntimeException ex)
                {
                    throw new PersistenceOperationFailedException(ex);
                }
                scope.Complete();
            }
        }

        /// <summary>
        /// Save changes (including aggregates) of each collection item into database. If some items were deleted from collection, that items
        /// will be deleted from database.
        /// </summary>
        /// <typeparam name="TInstance">Collection item type.</typeparam>
        /// <param name="collection">The collection to save.</param>
        /// <exception cref="System.ArgumentNullException"><paramref name="collection"/> is null reference.</exception>
        /// <exception cref="TypePersistenceNotSupportedException">The persistence of specific type is not supported.</exception>
        /// <exception cref="PersistenceOperationFailedException">The error is occured during data source operation</exception>
        /// <exception cref="VersionConflictException">The storing of instance failed because newest version of object already exist in data storage.</exception>
        /// <exception cref="IdentityReferenceIntegrityException">Different instance with same identifier already registered in session.</exception>
        /// <exception cref="ObjectDisposedException">Object is disposed.</exception>
        /// <exception cref="PersistenceOperationIsNotSupportedException"> Specified collection type is not supported.</exception>
        public void SaveCollection<TInstance>(IEnumerable<TInstance> collection) 
            where TInstance : class
        {
            CheckIsNotDisposed();
            if (Object.ReferenceEquals(collection, null))
                throw new ArgumentNullException("collection");
            using (var scope = OpenScope())
            {
                try
                {
                    _Provider.SaveCollection(collection);
                }
                catch (PersistingRuntimeException ex)
                {
                    throw new PersistenceOperationFailedException(ex);
                }
                scope.Complete();
            }
        }

        /// <summary>
        /// Save changes of all object in session into storage and accept all changes.
        /// Allows Unit Of Work scenario for working with session.
        /// </summary>
        /// <exception cref="PersistenceOperationFailedException">The error is occured during data source operation</exception>
        /// <exception cref="VersionConflictException">The store of object failed because newest version of object already exist in data storage.</exception>
        /// <exception cref="ObjectDisposedException">Object is disposed.</exception>
        /// <exception cref="T:OverStore.Exceptions.PersistenceOperationIsNotSupportedException">Operation is not supported because of configuration part responsible for this operation is missing or does not support specific functionallity.</exception>
        public void SaveAll()
        {
            CheckIsNotDisposed();
            using (var scope = OpenScope())
            {
                try
                {
                    _Provider.SaveAll();
                }
                catch (PersistingRuntimeException ex)
                {
                    throw new PersistenceOperationFailedException(ex);
                }

                scope.Complete();
            }
        }

        /// <summary>
        /// Mark object as deleted. Object will be removed during Save method call.
        /// For cancel deletion mark, use ReloadUsingData method.
        /// 
        /// The object will be available by it's identifier over all session lifetime, even
        /// it was deleted. If object leaves in storage after deleting,
        /// the object will be available by it's identifier even in another session. 
        /// </summary>
        /// <exception cref="System.ArgumentNullException">Instance is null.</exception>        
        /// <exception cref="TypePersistenceNotSupportedException">The persistence of specific type not supported.</exception>        
        /// <exception cref="ObjectDisposedException">Object is disposed.</exception>
        /// <exception cref="IdentityReferenceIntegrityException">Different instance with same identifier already registered in session.</exception>
        /// <exception cref="PersistenceOperationFailedException">Error occured during mark object as deleted.</exception>
        /// <exception cref="PersistenceOperationIsNotSupportedException">Operation is not supported because of configuration part responsible for this operation is missing or does not support specific functionallity.</exception>
        public void MarkAsDeleted<TInstance>(TInstance instance)
            where TInstance : class
        {
            CheckIsNotDisposed();
            if (instance == null)
                throw new ArgumentNullException("instance");

            using (var scope = OpenScope())
            {
                try
                {
                    _Provider.MarkAsDeleted<TInstance>(instance);
                }
                catch (PersistingRuntimeException ex)
                {
                    throw new PersistenceOperationFailedException(ex);
                }

                scope.Complete();
            }
        }

        /// <summary>
        /// Add object to session.
        /// The object now available by it's identifier.
        /// 
        /// If different instance of object with equal identifier already exist in session,
        /// the <see cref="IdentityReferenceIntegrityException"/> will be thrown.
        /// If instance already registered in context, method dooes not do any changes.
        /// </summary>
        /// <exception cref="System.ArgumentNullException">Instance is null.</exception>
        /// <exception cref="TypePersistenceNotSupportedException">The persistence of specific type not supported.</exception>
        /// <exception cref="IdentityReferenceIntegrityException">The different instance of same type with equal key already exists in session.</exception>
        /// <exception cref="ObjectDisposedException">Object is disposed.</exception>
        /// <exception cref="PersistenceOperationFailedException">Error occured during add object.</exception>
        /// <exception cref="PersistenceOperationIsNotSupportedException">Operation is not supported because of configuration part responsible for this operation is missing or does not support specific functionallity.</exception>
        public void Add<TInstance>(TInstance instance)
            where TInstance : class
        {
            CheckIsNotDisposed();
            if (instance == null)
                throw new ArgumentNullException("instance");

            using (var scope = OpenScope())
            {
                try
                {
                    _Provider.Add<TInstance>(instance);
                }
                catch (PersistingRuntimeException ex)
                {
                    throw new PersistenceOperationFailedException(ex);
                }
                scope.Complete();
            }
        }

        /// <summary>
        /// Joins specified object to session and apply changes made in <paramref name="instance"/>
        /// to object in session.
        /// </summary>
        /// <typeparam name="TInstance">OverrideDbType of object to attach.</typeparam>
        /// <param name="instance">Instance for attach to session.</param>
        /// <returns>
        /// Instance registered in session and contains applied changes.
        /// </returns>
        /// <exception cref="ArgumentNullException">Instance is null.</exception>
        /// <exception cref="TypePersistenceNotSupportedException">The persistence of specific type not supported.</exception>
        /// <exception cref="PersistenceOperationFailedException">The error is occured during data source operation</exception>
        /// <exception cref="ObjectDisposedException">Object is disposed.</exception>
        /// <exception cref="PersistenceOperationIsNotSupportedException">Operation is not supported because of configuration part responsible for this operation is missing or does not support specific functionallity.</exception>
        public TInstance Attach<TInstance>(TInstance instance)
            where TInstance : class
        {
            CheckIsNotDisposed();
            if (Object.ReferenceEquals(instance, null))
                throw new ArgumentNullException("instance");
            using (var scope = OpenScope())
            {
                try
                {
                    var result = _Provider.Attach<TInstance>(instance);
                    scope.Complete();
                    return result;
                }
                catch (PersistingRuntimeException ex)
                {
                    throw new PersistenceOperationFailedException(ex);
                }
            }
        }

        /// <summary>
        /// Get object with specified identifier. If object is not loaded, it will be
        /// loaded.
        /// </summary>
        /// <typeparam name="TInstance">The type of the instance.</typeparam>
        /// <typeparam name="TIdentifier">The type of the identifier.</typeparam>
        /// <param name="objectIdentifier">The object identifier.</param>
        /// <returns>
        /// The instance of <typeparamref name="TInstance"/> type with specified indentifier.
        /// </returns>
        /// <exception cref="System.ArgumentNullException">ObjectIdentifier is null</exception>
        /// <exception cref="TypePersistenceNotSupportedException">The persistence of TInstance type is not supported.</exception>
        /// <exception cref="IdentifierTypeIsInvalidException">OverrideDbType of identifier is invalid.</exception>
        /// <exception cref="PersistenceOperationFailedException">The error is occured during data source operation</exception>
        /// <exception cref="ObjectDisposedException">Object is disposed.</exception>
        /// <remarks>If object supports deffered loading, some of exception may be thrown when accessing object data.</remarks>
        public TInstance GetByIdentifier<TInstance, TIdentifier>(TIdentifier objectIdentifier)
            where TInstance : class
        {
            CheckIsNotDisposed();
            if (objectIdentifier == null)
                throw new ArgumentNullException("objectIdentifier");
            using (var scope = OpenScope())
            {
                try
                {
                    var result = _Provider.GetInstance<TInstance, TIdentifier>(objectIdentifier);
                    scope.Complete();
                    return result;
                }
                catch (PersistingRuntimeException ex)
                {
                    throw new PersistenceOperationFailedException(ex);
                }
            }
        }

        /// <summary>
        /// Get entity with specified identifier from data storage. If entity was requested before, it will be returned,
        /// otherwise entity with specified identifier will be loaded from data storage and returned.
        /// If there are no data for entitiy with such identifier in data storage, method returns null.
        /// </summary>
        /// <typeparam name="TInstance">The type of the instance.</typeparam>
        /// <typeparam name="TIdentifier">The type of the identifier.</typeparam>
        /// <param name="objectIdentifier">The object identifier.</param>
        /// <returns>
        /// The instance of <typeparamref name="TInstance"/> type with specified indentifier.
        /// </returns>
        /// <exception cref="System.ArgumentNullException"><paramref name="objectIdentifier"/> is null reference.</exception>
        /// <exception cref="TypePersistenceNotSupportedException">The persistence of TInstance type is not supported.</exception>
        /// <exception cref="IdentifierTypeIsInvalidException">OverrideDbType of identifier is invalid.</exception>
        /// <exception cref="PersistenceOperationFailedException">The error is occured during data source operation</exception>
        /// <exception cref="ObjectDisposedException">Object is disposed.</exception>
        /// <remarks>If object supports deffered loading, some of exception may be thrown when accessing object data.</remarks>
        public TInstance GetByIdentifierOrDefault<TInstance, TIdentifier>(TIdentifier objectIdentifier)
            where TInstance : class
        {
            CheckIsNotDisposed();
            if (objectIdentifier == null)
                throw new ArgumentNullException("objectIdentifier");
            using (var scope = OpenScope())
            {
                try
                {
                    var result = _Provider.GetInstanceOrDefault<TInstance, TIdentifier>(objectIdentifier);
                    scope.Complete();
                    return result;
                }
                catch (PersistingRuntimeException ex)
                {
                    throw new PersistenceOperationFailedException(ex);
                }
            }
        }

        #endregion

        #region ObjectReader

        /// <summary>
        /// Create object reader with specified parameter. Object reader provides sequential forward-only
        /// access to set of objects.
        /// </summary>
        /// <typeparam name="TItem">The type of the item to iterate.</typeparam>
        /// <typeparam name="TParameter">The type of the parameter uses for load object set from data storage.</typeparam>
        /// <param name="parameter">The parameter uses for load object set from data storage.</param>
        /// <returns>
        /// The iterator throught set of object from data storage.
        /// </returns>
        /// <exception cref="System.ArgumentNullException">Object type is null.</exception>
        /// <exception cref="System.ArgumentNullException">Parameters is null.</exception>
        /// <exception cref="ParameterTypeNotSupportedException">Parameters has an unsupported type</exception>
        /// <exception cref="TypePersistenceNotSupportedException">The reading of objects of specified type is not supported.</exception>
        /// <exception cref="PersistenceOperationFailedException">The error is occured during data source operation</exception>
        /// <exception cref="ObjectDisposedException">Object is disposed.</exception>
        /// <exception cref="PersistingRuntimeException">Error occured during creating object reader.</exception>
        public IEnumerable<TItem> CreateReader<TItem, TParameter>(TParameter parameter)
            where TItem : class
            where TParameter : class
        {
            CheckIsNotDisposed();
            if (ReferenceEquals(parameter, null))
                throw new ArgumentNullException("parameter");
            try
            {
                return _Provider.ExecuteObjectReader<TItem, TParameter>(parameter);
            }
            catch (PersistingRuntimeException ex)
            {
                throw new PersistenceOperationFailedException(ex);
            }
        }

        /// <summary>
        /// Creates iterator throught collection of object in storage with specified parameter. Object reader provides sequential forward-only
        /// access to set of objects.
        /// </summary>
        /// <typeparam name="TItem">The type of the item to iterate.</typeparam>
        /// <returns>
        /// The iterator throught set of object from data storage.
        /// </returns>
        /// <exception cref="System.ArgumentNullException">Object type is null.</exception>
        /// <exception cref="System.ArgumentNullException">Parameters is null.</exception>
        /// <exception cref="ParameterTypeNotSupportedException">Parameters has an unsupported type</exception>
        /// <exception cref="TypePersistenceNotSupportedException">The reading of objects of specified type is not supported.</exception>
        /// <exception cref="PersistenceOperationFailedException">The error is occured during data source operation</exception>
        /// <exception cref="ObjectDisposedException">Object is disposed.</exception>
        public IEnumerable<TItem> CreateReader<TItem>()
            where TItem : class
        {
            CheckIsNotDisposed();
            try
            {
                return _Provider.ExecuteObjectReader<TItem>();
            }
            catch (PersistingRuntimeException ex)
            {
                throw new PersistenceOperationFailedException(ex);
            }
        }

        #endregion

        /// <summary>
        /// Starts operation scope for the current session.
        /// </summary>
        public OperationScope OpenScope()
        {
            CheckIsNotDisposed();
            return _Storage.OpenScope();
        }

        /// <summary>
        /// Throws the <see cref="System.ObjectDisposedException"/> if instance was disposed.
        /// </summary>
        private void CheckIsNotDisposed()
        {
            if (_Disposed)
                throw new ObjectDisposedException("Persistence Session");
        }
    }
}